<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Dan Dean</title>
        <link>https://dandean.com</link>
        <description>The personal website of Dan Dean</description>
        <lastBuildDate>Tue, 10 Feb 2026 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <copyright>All rights reserved, Dan Dean, 2026</copyright>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confident in working on features.</p>
<p><strong>Obsolete technologies.</strong> The framework was built on top of MooTools, which heavily modifies the native runtime environment by extending native classes and exposing a large number of globals. Using MooTools as the core made it difficult to use most other open source client-side technologies (which I&#39;ll talk about later). Aside from the technical issues, MooTools made hiring difficult. Nobody building state of the art web applications wants to go anywhere near MooTools.</p>
<p>MooTools used its own package manager called Packager, which was built with PHP. Packager was awful to work with so we wrote our own drop-in replacement in Ruby called Plums. Only one person on the team really understood how Plums worked, and nobody knew how Packager worked. Whenever a module packaging error occurred people would struggle for hours to resolve the issue.</p>
<p>I can&#39;t describe how bleak it is trying to build a state of the art product on these tools when open source alternatives are blossoming around every corner. The problems our in-house tools were solving had been solved over and over, and in better ways, than our own.</p>
<p><strong>Poor separation of domains.</strong> The middle-tier of the banking application was a Ruby application. The Ruby application would then do its own work and eventually make requests to the backend services.</p>
<p>The fundamental problem was that the Ruby application should have been a lean proxy to the services, but was instead doing far too much work. Engineer would commonly re-write client requests at this layer before sending them to services, and also rewrite service responses before sending them back to the client. This was sometimes done to accommodate the inflexibility of the client-side framework, other times to get something done sooner rather than waiting on the backend team to have time to build it out in the services layer.</p>
<p>A side effect of this middle tier is that important details would often get dropped between the client-side and the services. For instance, HTTP headers set in the client were almost never sent to the services, which made many requests less efficient because things like <code>if-modified-since</code> couldn&#39;t be taken into account. A common issue is that the middle-tier endpoint for a specific request wouldn&#39;t have taken every HTTP response code from the service into account, so basic things like 429 Too Many Requests (aka rate-limiting) responses would appear in the client application as 500 Server Error, or get swallowed altogether and appear as 200 with an empty body.</p>
<p>The organizational impact of this middle-tier was largely unacknowledged but immense. We essentially had two APIs: one official API with documentation and processes for proposal, critique, and acceptance, the other completely ad hoc and undocumented. Instead of bringing API needs to the backend team, this team was self-servicing half-baked workarounds.</p>
<p><strong>Humans and Feelings.</strong> Everything about the application was bespoke – many people in the organization had put months and even years into inventing it from scratch. Navigating how to speak to all of the above issues and proposing to replace it while honoring their work was a daunting task.</p>
<!--
**Implicit coupling made change impossible.**
**Imperative operations.**
-->

<h2>Solution</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-explosion.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://www.flickr.com/photos/bryanburke/2854366734">Photo by Bryan Burke</a>
</div>

<p>the things which were recurring pain points we all dealt with. We regularly had to write complex workarounds for using the same UI component in multiple places in the application. The process could take hours and often resulted in a long-tail of odd UI and state bugs. I built a very small proof of concept application which showed how React could render any component in any nested structure any number of times, and state changes would propagate to every instance based on the central state tree. That one little example made most of the team understand how much better things could be.</p>
<p><strong>Explaining the Problem to the Company.</strong> Once the team was on board I put together a talk, &quot;The Future of Our Web Applications&quot;, which was presented at our quarterly engineering conference. I spelled out in detail how much time and effort we were putting into dealing with bespoke technologies instead of shipping user-facing features. I outlined each of the things which were failing us, what we would replace them with, and how that would lead to more Product focus.</p>
<p><strong>Incremental change and then rebuilding in parallel.</strong> Our goal was to incrementally rebuild the application in place so that we could continuously ship to users without blocking Product work.</p>
<p>We sliced all of the standalone screens and rebuilt them, so navigating between pages would be navigating between client-side applications. The vast majoring of the banking application, unfortunately, existed in the transaction sidebar (bill pay, ACH transfer, instant payments, support chats, etc). In order to incrementally rebuild this features we&#39;d have to ship one sidebar feature at a time in the same client-side environment as the MooTools application. We ran into a wall of incompatibility. Some of the native browser objects React depended were modified by the MooTools runtime, so React would throw errors while rendering. Our in-house MooTools router fought with React-Router for control in unexpected ways. Making progress became impossible.</p>
<p>After getting stalled for about two months we made the decision to rebuild all of the sidebar features in one go and ship them together; our users would stop getting updates for a while, but they&#39;d get the fully-functioning rebuild <em>much</em> sooner. This would also allow the entire team to return to working on new features sooner, given the slow pace of the incremental rebuild work.</p>
<p><strong>Move functionality to the services.</strong> Many of the application&#39;s fundamental problems were solved by replacing our tools with React (+ Redux), etc., and using npm instead of Plums. The issue of a poor separation of domain was a different story. Nobody on the team wanted to continue making the same mistakes within our middle-tier Ruby application. As we worked through the rebuilt UI we collaborated closes with Data and Backend engineers to move functionality into the services. The Search feature got a dedicated backend service (so now mobile clients get the same search results as the web app), new transaction paging endpoints were created, all new middle-tier endpoints were lean pass-through proxies straight to the services, and some features got dropped because the cost to replicate wasn&#39;t worth the tradeoff.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/rebuilding-a-bespoke-web-application-for-sustainability-rainbows.png" alt=""></p>
<div style="text-align:right; font-size:0.6em; margin:-2.5em 0 1em 0">
<a href="https://commons.wikimedia.org/wiki/File:Double-alaskan-rainbow.jpg">Photo by Eric Rolph</a>
</div>

<p>The new React + Redux application has proven to be a huge boon to productivity and overall happiness. We ship features and changes faster and with fewer bugs. Because we&#39;re using standard tools we&#39;re able to take advantage of the wider ecosystem, which has led to the adoption of ESLint, Webpack, charting libraries, validation libraries, and so on. These tools have helped us to ship higher quality code while focusing solely on user-facing features in support of the larger product vision.</p>
]]></content:encoded>
            <category>web</category>
            <category>architecture</category>
            <category>planning</category>
            <category>team</category>
            <enclosure url="https://dandean.com/rebuilding-a-bespoke-web-application-for-sustainability-explosion" length="0" type="image//rebuilding-a-bespoke-web-application-for-sustainability-explosion"/>
        </item>
        <item>
            <title><![CDATA[🧐 TIL: global .gitignore]]></title>
            <link>https://dandean.com/posts/til-global-gitignore</link>
            <guid isPermaLink="true">https://dandean.com/posts/til-global-gitignore</guid>
            <pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Today I learned about global <code>.gitgnore</code> files.</p>
<p>According to <code>git</code> docs this is configured with the <code>core.excludesfile</code>.</p>
<p>To see if this is set:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --get</span><span style="color:#98C379"> core.excludesfile</span></span></code></pre>
</code></pre><p>If so, any pattern in that file will be globally applied. Unfortunately, I didn&#39;t check that before monkeying with things, so I have no idea if I&#39;ve changed my system accidentally.</p>
<p>From the <a href="https://git-scm.com/docs/gitignore">docs</a> (which I finally found and read):</p>
<blockquote>
<p>Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user’s editor of choice) generally go into a file specified by <code>core.excludesFile</code> in the user’s <code>~/.gitconfig</code>. Its default value is <code>$XDG_CONFIG_HOME/git/ignore</code>. If <code>$XDG_CONFIG_HOME</code> is either not set or empty, <code>$HOME/.config/git/ignore</code> is used instead.</p>
</blockquote>
<p>On my system:</p>
<ul>
<li><code>$XDG_CONFIG_HOME</code> is not set</li>
<li><code>$HOME/.config/git/ignore</code> does not exist</li>
</ul>
<p>So I think this means that I had no previously-configured global ignore patterns? Maybe???</p>
<p>In any event, I&#39;ve now created <code>$HOME/.config/git/ignore</code> and configured <code>git</code> to specifically use it:</p>
<pre><code class="language-sh"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#B57EDC">git</span><span style="color:#98C379"> config</span><span style="color:#56B6C2"> --global</span><span style="color:#98C379"> core.excludesfile</span><span style="color:#C6CCD7"> $HOME</span><span style="color:#98C379">/.config/git/ignore</span></span></code></pre>
</code></pre><p>I&#39;ll find out in the coming days if new files start showing up in my various repos when I run <code>git status</code>. 🤷</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/316dda521546d1f4a87a474b1b434ea6" length="0" type="image//316dda521546d1f4a87a474b1b434ea6"/>
        </item>
        <item>
            <title><![CDATA[The Problem with Early Warnings]]></title>
            <link>https://dandean.com/posts/early-warnings</link>
            <guid isPermaLink="true">https://dandean.com/posts/early-warnings</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>
  .page-header {
    height: auto;
    min-height: 30vh;
  }
  main {
    padding-left: 140px;

    p {
      font-family: Georgia, serif;
    }
  }
  @media screen and (max-width: 896px) {
    main {
      padding-left: 20px;
    }
  }
</style>

<p>People don&#39;t like to leave a party<br>unless the house is actually<br>on fire. Even then, if the flames<br>are far enough away<br>to be pretty, they&#39;ll finish<br>their drink, take one more pass<br>at the hors d&#39;oeuvres.<br>How things happen has always been<br>unclear. Hurricanes begin<br>in a place where no one lives.<br>Agents of the government start<br>to wear masks. <em>Fascism</em> is<br>a word my neighbors won&#39;t use<br>yet. They are following<br>the law, they say, and the sirens<br>are coming for someone else.  </p>
<p>– CHARLES RAFFERTY</p>
<h3>Prepare:</h3>
<ul>
<li><a href="https://waisn.org/what-we-do/deportation-defense/rapid-response/">Become a rapid responder in your community!</a></li>
<li><a href="https://linktr.ee/wa.whistles">Join with your organized neighbors!</a></li>
</ul>
]]></content:encoded>
            <category>fascism</category>
            <category>poetry</category>
            <category>community</category>
            <enclosure url="https://dandean.com/whistle-2" length="0" type="image//whistle-2"/>
        </item>
        <item>
            <title><![CDATA[🎹 Crushingly beautiful]]></title>
            <link>https://dandean.com/posts/big-thief-mary</link>
            <guid isPermaLink="true">https://dandean.com/posts/big-thief-mary</guid>
            <pubDate>Sat, 10 Jan 2026 08:01:22 GMT</pubDate>
            <content:encoded><![CDATA[<iframe allow="autoplay *; encrypted-media *;" frameborder="0" height="150" style="width:100%;max-width:660px;overflow:hidden;background:transparent;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.music.apple.com/us/album/mary/1751255074?i=1751255509"></iframe>

]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/big-thief-capacity" length="0" type="image//big-thief-capacity"/>
        </item>
        <item>
            <title><![CDATA[🎹 You've got pollen on your nose]]></title>
            <link>https://dandean.com/posts/mirah-pollen</link>
            <guid isPermaLink="true">https://dandean.com/posts/mirah-pollen</guid>
            <pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>An incredibly adorable song that you should listen to.</p>
<iframe style="border: 0; width: 500px; height: 120px; display:block;" src="https://bandcamp.com/EmbeddedPlayer/album=726682514/size=large/bgcol=333333/linkcol=0f91ff/tracklist=false/artwork=small/track=3018758225/transparent=true/" seamless><a href="https://mirah.bandcamp.com/album/you-think-its-like-this-but-really-its-like-this">You Think Its Like This... But Really Its Like This by Mirah</a></iframe>
]]></content:encoded>
            <category>music</category>
            <enclosure url="https://dandean.com/mirah" length="0" type="image//mirah"/>
        </item>
        <item>
            <title><![CDATA[Collecting a list of Nest RouterModule path prefixes]]></title>
            <link>https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</link>
            <guid isPermaLink="true">https://dandean.com/posts/collecting-a-list-of-nest-routermodule-path-prefixes</guid>
            <pubDate>Sun, 08 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In Nest we declare path components on <code>@Controller(&lt;path&gt;)</code> and method decorators like <code>@Get(&lt;path&gt;)</code>, <code>@Post(&lt;path&gt;)</code>, etc. But it&#39;s also possible to declare a &quot;path prefix&quot; which applies to all controllers in the module via <code>RouterModule</code>:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#C6CCD7">RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">({path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">/foo</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">MyModule</span><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>I&#39;m in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With <code>DiscoveryService</code> it&#39;s possible to get path components from <code>@Controller()</code> and method decorators, but not <code>RouterModule</code> path prefixes.</p>
<p>I spent some time digging through Nest source code. There is a <code>GraphInspector</code> class, and that looked promising, but I couldn&#39;t find a way to make it useful here. There&#39;s also a <code>RouterExplorer</code> class, but it&#39;s internal.</p>
<p>In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of <code>&quot;__module_path__&lt;bunchofstuff&gt;&quot;</code>, and on that is the path prefix!</p>
<p>What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let&#39;s declare a minimal module structure to prove out a solution:</p>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">Controller</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Get</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">Module</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">OnApplicationBootstrap</span><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/common</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">import</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#C6CCD7">  ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  NestFactory</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  Reflector</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">  RouterModule</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#A9B2C3">} </span><span style="color:#E06C75">from</span><span style="color:#A9B2C3"> '</span><span style="color:#98C379">@nestjs/core</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller1</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller1</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module1</span><span style="color:#A9B2C3"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Controller</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">controller-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Controller2</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#A9B2C3">  @</span><span style="color:#B57EDC">Get</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">index</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#B57EDC">  index</span><span style="color:#A9B2C3">() {}</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  controllers: [</span><span style="color:#C6CCD7">Controller2</span><span style="color:#A9B2C3">],</span></span>
<span class="line"><span style="color:#A9B2C3">  providers: [],</span></span>
<span class="line"><span style="color:#A9B2C3">  exports: [],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#61AFEF">class</span><span style="color:#E5C07B"> Module2</span><span style="color:#A9B2C3"> {}</span></span></code></pre>
</code></pre><p>The solution will involve:</p>
<ul>
<li>Waiting for the module graph to be completely built</li>
<li>Traversal of the module graph</li>
<li>Identify each module with <code>__module_path__...</code> metadata</li>
<li>Pull out the associated path, filtering down the set of modules to only those with path prefixes</li>
</ul>
<pre><code class="language-ts"><pre class="shiki plastic" style="background-color:#21252B;color:#A9B2C3" tabindex="0"><code><span class="line"><span style="color:#A9B2C3">@</span><span style="color:#B57EDC">Module</span><span style="color:#A9B2C3">({</span></span>
<span class="line"><span style="color:#A9B2C3">  imports: [</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Here we see each of our modules imported and registered:</span></span>
<span class="line"><span style="color:#C6CCD7">    Module1</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-1</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module1</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#C6CCD7">    Module2</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#C6CCD7">    RouterModule</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">register</span><span style="color:#A9B2C3">([{path: </span><span style="color:#A9B2C3">'</span><span style="color:#98C379">module-2</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">, module: </span><span style="color:#C6CCD7">Module2</span><span style="color:#A9B2C3">}]),</span></span>
<span class="line"><span style="color:#A9B2C3">  ],</span></span>
<span class="line"><span style="color:#A9B2C3">})</span></span>
<span class="line"><span style="color:#E06C75">export</span><span style="color:#61AFEF"> class</span><span style="color:#E5C07B"> RouteDiscoveryAppModule</span><span style="color:#61AFEF"> implements</span><span style="color:#D19A66"> OnApplicationBootstrap</span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  constructor</span><span style="color:#A9B2C3">(</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject the global ModuleContainer so we can traverse modules:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> modulesContainer</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> ModulesContainer</span><span style="color:#A9B2C3">,</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">    // Inject Reflector so we can get metadata:</span></span>
<span class="line"><span style="color:#61AFEF">    private</span><span style="color:#C6CCD7"> reflector</span><span style="color:#E06C75">:</span><span style="color:#E5C07B"> Reflector</span></span>
<span class="line"><span style="color:#A9B2C3">  ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B57EDC">  onApplicationBootstrap</span><span style="color:#A9B2C3">() {</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modules</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Array</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">from</span><span style="color:#A9B2C3">(</span><span style="color:#E06C75">this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">modulesContainer</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">values</span><span style="color:#A9B2C3">());</span></span>
<span class="line"><span style="color:#61AFEF">    const</span><span style="color:#C6CCD7"> modulesWithPathPrefix</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> modules</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">map</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Use the native `Reflect` API to get all decorator metadata</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // on the modules's `class` (eg, metatype):</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> Reflect</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">getMetadataKeys</span><span style="color:#A9B2C3">(</span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // Look for a metadata key so we can use it to get path metadata:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> pathKey</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> metadataKeys</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">find</span><span style="color:#A9B2C3">((</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">) </span><span style="color:#61AFEF">=></span></span>
<span class="line"><span style="color:#B57EDC">          String</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">key</span><span style="color:#A9B2C3">).</span><span style="color:#B57EDC">startsWith</span><span style="color:#A9B2C3">(</span><span style="color:#A9B2C3">'</span><span style="color:#98C379">__module_path__</span><span style="color:#A9B2C3">'</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#A9B2C3">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">        // If we found a path key, pull the path out:</span></span>
<span class="line"><span style="color:#61AFEF">        const</span><span style="color:#C6CCD7"> path</span><span style="color:#E06C75"> =</span><span style="color:#C6CCD7"> pathKey</span></span>
<span class="line"><span style="color:#E06C75">          ?</span><span style="color:#E06C75"> this</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">reflector</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">get</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">pathKey</span><span style="color:#A9B2C3">, </span><span style="color:#E5C07B">module</span><span style="color:#A9B2C3">.</span><span style="color:#C6CCD7">metatype</span><span style="color:#A9B2C3">)</span></span>
<span class="line"><span style="color:#E06C75">          :</span><span style="color:#56B6C2"> undefined</span><span style="color:#A9B2C3">;</span></span>
<span class="line"><span style="color:#E06C75">        return</span><span style="color:#A9B2C3"> {</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">, </span><span style="color:#C6CCD7">module</span><span style="color:#A9B2C3">};</span></span>
<span class="line"><span style="color:#A9B2C3">      })</span></span>
<span class="line"><span style="color:#5F6672;font-style:italic">      // Filter out all modules with no paths:</span></span>
<span class="line"><span style="color:#A9B2C3">      .</span><span style="color:#B57EDC">filter</span><span style="color:#A9B2C3">(({</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">}) </span><span style="color:#61AFEF">=></span><span style="color:#B57EDC"> Boolean</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">path</span><span style="color:#A9B2C3">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C6CCD7">    console</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">log</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">modulesWithPathPrefix</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">  }</span></span>
<span class="line"><span style="color:#A9B2C3">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#5F6672;font-style:italic">// Run the program:</span></span>
<span class="line"><span style="color:#61AFEF">Promise</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">resolve</span><span style="color:#A9B2C3">().</span><span style="color:#B57EDC">then</span><span style="color:#A9B2C3">(</span><span style="color:#61AFEF">async</span><span style="color:#A9B2C3"> () </span><span style="color:#61AFEF">=></span><span style="color:#A9B2C3"> {</span></span>
<span class="line"><span style="color:#61AFEF">  const</span><span style="color:#C6CCD7"> app</span><span style="color:#E06C75"> =</span><span style="color:#E06C75"> await</span><span style="color:#C6CCD7"> NestFactory</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">create</span><span style="color:#A9B2C3">(</span><span style="color:#C6CCD7">RouteDiscoveryAppModule</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#C6CCD7">  app</span><span style="color:#A9B2C3">.</span><span style="color:#B57EDC">listen</span><span style="color:#A9B2C3">(</span><span style="color:#56B6C2">3001</span><span style="color:#A9B2C3">);</span></span>
<span class="line"><span style="color:#A9B2C3">});</span></span></code></pre>
</code></pre><p>It&#39;s pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.</p>
]]></content:encoded>
            <category>nestjs</category>
            <category>nodejs</category>
            <category>programming</category>
            <enclosure url="https://dandean.com/collecting-a-list-of-nest-routermodule-path-prefixes-module-paths" length="0" type="image//collecting-a-list-of-nest-routermodule-path-prefixes-module-paths"/>
        </item>
        <item>
            <title><![CDATA[Using Obsidian for Mastodon and my website]]></title>
            <link>https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-obsidian-for-mastodon-and-my-website</guid>
            <pubDate>Sat, 31 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I still miss some of the product polish of Notion, but the customizability and community ecosystem for Obsidian.md are <strong>amazing</strong>.</p>
<p>There are two plugins I&#39;m really excited about. The first lets you post to Mastodon directly from Obsidian. The plugin manages media attachments and thread separators (complete with character counts synced to your server&#39;s settings!):</p>
<p><a href="https://github.com/elpamplina/mastodon-threading">https://github.com/elpamplina/mastodon-threading</a></p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post.png" alt=""></p>
<blockquote>
<p>A screenshot of this thread as seen in Obsidian.md, showing thread separators and character counts.</p>
</blockquote>
<p>The other is a plugin which sends the current note&#39;s raw data and attachments to a webhook:</p>
<p><a href="https://github.com/Masterb1234/obsidian-post-webhook">https://github.com/Masterb1234/obsidian-post-webhook</a></p>
<p>Put together, these drastically reduce the amount of code I need to write in order to fully own my stuff, while keeping is centralized, easy to access, and not locked in some piece of shit VC-backed silo.</p>
]]></content:encoded>
            <category>posse</category>
            <category>obsidian</category>
            <enclosure url="https://dandean.com/using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post" length="0" type="image//using-obsidian-for-mastodon-and-my-website-screenshot-of-obsidian-mastodon-post"/>
        </item>
        <item>
            <title><![CDATA[Splice Design System]]></title>
            <link>https://dandean.com/posts/splice-design-system</link>
            <guid isPermaLink="true">https://dandean.com/posts/splice-design-system</guid>
            <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><strong>In <a href="/projects/systems-greater-tools">Systems &gt; Tools</a> I explored how systems make it possible to derive value from tools</strong>, and how tools alone rarely lead to an organization&#39;s success. The work I did on Splice&#39;s design system is a good example of this in practice.</p>
<p>Before my time at Splice there had been a few attempts at building a design system. In the last attempt styles had been audited and SCSS modules were created to encapsulate them. A backlog captured everything to be migrated. These styles were copied to a standalone repo and npm module to make them consumable by any application. There was even a sort of storybook-like preview site to show these styles decoupled from an application.</p>
<p>What Splice had at that point was a collection of tools, but nothing tying those tools to processes, nothing driving individual contributors to adopt them or participate in their development, nothing connecting their development to their visual origins within the design team. These things had the potential to help solve some <em>software challenges</em> associated with building user interfaces, but they did not add up to a <em>system</em>. What was lacking was a broader view of the challenge at hand, and without that broader view success was elusive.</p>
<h2>Aside: I Wasn&#39;t Hired to Build a Design System</h2>
<p>I was hired into Splice&#39;s Search &amp; Recommendations team. I started on a Monday and the following Friday the team was dissolved as part of a broader engineering reorganization.</p>
<p>😬</p>
<p>My first project on my <em>new</em> team was to build the new &quot;Pack Card&quot; implementation, and the task was surprisingly difficult. CSS was everywhere: leaking out of global stylesheets associated with an embedded legacy AngularJS app, sprinkles of Bootstrap, a collection of SCSS selectors in a &quot;splice-styles&quot; directory, and in individual CSS files associated with specific Angular components.</p>
<p>Splice&#39;s current design system was motivated by the situation I found myself in. There was no obvious entrypoint into how to build Pack Cards in a coherent way. I could have just crammed some CSS together and picked up the next ticket - nobody would have batted an eye – but stapling yet more onto an incoherent system is something I have a hard time bringing myself to do.</p>
<p>I took this as an opportunity to not just solve the immediate need of shipping the &quot;Pack Card&quot; redesign, but to explore the problem space and prove out a solution to Splice&#39;s systemic challenges for designing and delivering user interfaces. The problem wasn&#39;t how to build the Pack Cards, it was to figure out how to make it less painful to build at Splice in general.</p>
<h2>Design System Tunnel Vision</h2>
<p>In many organizations the &quot;design system&quot; describes a set of artifacts which are owned by either of the Design or Engineering teams. The owner is primarily concerned with their own team&#39;s needs – other teams must extract what they can from that to inform their own work.</p>
<p>For instance, if the design system is wholly-owned by the Design Team, then the artifacts are often a set of PDFs (or PSDs, or Figmas) describing in great detail every aspect of how things should be implemented, be it brand colors, font families, font sizes, design patterns, or anything else all the way to conventions around language and tone.</p>
<p>In that context, the engineering team takes these specifications and builds implementations which reflect them. The code implementations may or may not encapsulate the design system in a set of tools to reduce work. The focus here is first and foremost on the <em>specification</em>, so let&#39;s call this approach the &quot;Design Specification&quot; approach to Design Systems.</p>
<p>On the other hand, if the design system is wholly-owned by the Engineering Team, then the artifacts are usually some sort of component library (maybe a some React or Swift components, maybe some CSS specified in BEM, etc). The focus is on engineering efficiency, and ensuring that engineers can implement designs consistently without having to constantly rebuild things, and without having to have designers watch over their every move. Let&#39;s call this approach the &quot;Component Library&quot; approach to Design Systems.</p>
<p>Both of these approaches provide value to an organization. They reduce variance, they limit duplicated effort, they improve communication across teams. They&#39;re also both incredibly limited: they reenforce Design and Engineering silos instead of fostering continuous dialog and collaboration; they speak to one team&#39;s needs at the expense of the other&#39;s. And on the negative side, they often work as leverage for telling the other team &quot;no&quot; without providing a way for that team to feed requirements into it.</p>
<h2>Design System as Organizational Process</h2>
<p>What if instead of describing a set of artifacts, we took &quot;Design System&quot; to mean the system by which design goes from concept to delivery, including a continuous feedback loop between Design and Engineering which facilitates product evolution? This changes everything about how both teams approach their work.</p>
<p>&quot;Design System&quot; is now a higher-level concept which encompasses all of the artifacts previously identified, but also describes how those artifacts relate to each other, each individual&#39;s role in driving their evolution across each layer, and how to affect change on those artifacts in a coherent and collaborative way.</p>
<p>This approach to a Design System is more comprehensive and spans more of the organization, and consists of multiple complimentary &quot;layers&quot;.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-layers.png" alt=""></p>
<h3>Layer One: The Pattern Library</h3>
<p><strong>The Pattern Library</strong> is a set of technology-agnostic Design and Functional Specifications which describe repeatable patterns (colors, fonts, sizes, buttons, forms, cards, etc) spanning the &quot;atomic design&quot; nomenclature. Primarily owned by the Design Team.</p>
<p>The pattern library will describe design patterns broadly, going into detail about how they are applied differently based on context. A carousel design pattern might specify the overall look and feel, along with the kind of content it can contain, how &quot;previous&quot; and &quot;next&quot; behaviors work, how it behaves in mouse vs touch contexts, and how it may change based on context, such as if it&#39;s on web, iOS, or Android.</p>
<p>The pattern library usually exists &quot;pre-implementation&quot; in something like Notion and/or Figma, where general discussion and exploration of the pattern can take place, and where individuals can be directed for their own education about what currently exists and <em>why</em>.</p>
<blockquote>
<p>That &quot;why&quot; is critical because the pattern library helps us <em>constrain the number of design patterns</em>, and this improves communication with the customer. For example, if we have a Video Card design pattern, that card <em>is the design language</em> within our product which our customers see over and over again, and for which they will gain an understanding and set of expectations. If any product team needs to show a video they will be able to rely on those expectations for interacting with the customer. If they go their own way and make arbitrary decisions about how to display a video, then they&#39;re going to have a much harder time communicating with the customer. This is why constraining design patterns is <em>good</em>.</p>
</blockquote>
<p>The pattern library helps us inspect the natural inclination to assume our use cases are unique. When new use cases arise they can be held up against the pattern library either be thoughtfully rolled into existing design patterns, or become new design patterns if they truly are unique.</p>
<h3>Layer Two: Component Libraries</h3>
<p><strong>Component Libraries</strong> are concrete implementations of the Pattern Library in specific technologies (React, Angular, Flutter, Swift, Webflow, etc). Primarily owned by Engineering Teams. There should be nothing in a component library which doesn&#39;t also exist in the pattern library.</p>
<p>Component libraries are the bridge between the pattern library and an organization&#39;s various products. They contain functional implementations of design patterns which can be assembled to realize product features efficiently. When an engineer is tasked with building a feature, this should be their first stop, and when they come up against the edges of a particular library&#39;s capabilities, they will contribute back to it to more fully realize a component as a fully fleshed-out design pattern.</p>
<p>Component libraries are the layer at which engineers participate in the design system feedback loop to bring engineering knowledge back to the pattern library. Engineers will have deep experience in their particular stack, and they can use that to evolve a design pattern to be more fit for purpose, rather than bolted on. For example, the engineer may raise up requirements around assistive devices (ie, alt text, focus, etc), or contextual UI expectations associated with a particular device (ie, native iOS select interfaces, etc).</p>
<p>This is also an opportunity to push back on design pattern creep. If a feature&#39;s design specification looks a whole lot like another component in function, if not form, an engineer should be encouraged to bring that back to the pattern library rather than just implementing anything they&#39;re handed. This brings us to...</p>
<h3>Layer Three: The Feedback Loop</h3>
<p>Pattern and Component Libraries are both points within the design → engineering → design feedback loop. <strong>The Feedback Loop</strong> is what makes it possible to gain the most value out of a Design System because it allows designers and engineers to participate in the evolution and refinement of the artifacts coming out of the system.</p>
<p>In a well-functioning feedback loop designers and engineers are learning from each other, gaining empathy for each other&#39;s concerns, and related work outside of their local silos. The same is true between engineers across stacks as well – if iOS and Web engineers are both contributing feedback to the same design patterns they will have more opportunities to understand how those design patterns apply across different technology contexts.</p>
<p>One mechanism for facilitating the feedback loop is to have a regular (optional) cross-disciplinary Design System Office Hours where engineers and designers can bring questions, solicit input, and help others.</p>
<h3>Layer Four: Accountability</h3>
<p>Individuals will <em>always</em> prioritize what they are accountable for – this is often the thing which kills an organization&#39;s design system aspirations.</p>
<p>Design systems produce <em>capabilities</em>, not <em>features</em>. They slow down the delivery of <em>the current</em> feature (the team has to first identify and extract the capabilities), but they make all subsequent features vastly easier to deliver because the team now has a set of capabilities to draw from. If a team is only ever accountable for the delivery of the <em>current</em> feature, then the incentives just do not exist to build a design system. This is where the organization&#39;s leadership comes in.</p>
<p>Design and Engineering management <em>must</em> build participation in, and contribution to, the Design System into their evaluation frameworks. Not just &quot;what features did you deliver&quot;, but &quot;what capabilities&quot;. During the review cycle designers and engineers should be able to point to an array of others who have been able to deliver on top of their own work within the design system, and they should be celebrated for it.</p>
<h2>From Proof of Concept Component Library to Organizational Directive</h2>
<p>Getting from that first desire to make it easier to build things like Pack Cards, to transforming the way Splice approached the idea of a design system, and software delivery in general, was not a straight line. I honestly kind of fumbled into it with a lot of luck and good timing.</p>
<p>I produced that first component library proof of concept in collaboration with my peers. It worked great, but it was still just a singular tool, not a system. A few months later the engineering reorganization continued as a new CTO and Director of Engineering were brought on. Their primary focus was on making it possible for Splice to more predictably deliver software, and unlike many new leaders, they opened the table to any and all ideas for how to achieve these goals. This ended up being the opportunity we needed.</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/splice-design-system-design-system-rfc.png" alt=""></p>
<p>An RFC was written up, senior leadership approved, and individual contributors were largely on board. Myself and then Design Director <a href="https://jayschaul.com">Jay Schaul</a> were tasked with leading the initiative and helping people from each of our respective domains begin the process of starting to think and work in this way.</p>
<p>Three key choices ended up being effective leverage for getting the organizational process up and running, acting as an onramp for individuals from across the organization into this new process:</p>
<ol>
<li>On the design side, Jay created the working framework for the Pattern Library which consisted of an organized area of Figma, where existing work was brought into the pattern library structure, and a &quot;proposal&quot; area of Notion, where design patterns could be proposed using a starter template with standard areas to be fleshed out by contributors.</li>
<li>On the engineering side, I created an Angular library where components and reusable CSS could live and be consumable from any of our web-based applications, ported our nascent SCSS modules into it, moved our Pack Cards into it, and stood up an instance of Storybook to make it possible to view everything in the component library and to operate as a component development environment decoupled from the applications.</li>
<li>Jay and I started optional bi-weekly design system check-in meetings (aka, design system office hours).</li>
</ol>
<h2>Progress, Setbacks, and Bus Factors</h2>
<p>Much progress has been made: we&#39;re able to build features faster and more consistently, to reuse code across client applications, and to release those features across clients without significant additional effort.</p>
<p>But we&#39;re far from done. We have yet to fold in our mobile team, and as of late it feels like our feedback loop is fraying at the edges. The work now is to operate on the system to address its shortcomings. Much like we evolve design patterns, we will evolve our Design System to account for new organizational requirements without having to start from scratch.</p>
<p>Someone once said to me that for a process or system be sustainable it must not depend on the presence of any particular individual. No employee is forever. We need to continue to build the organizational muscle memory to make our Design System process more resilient to staffing changes, and less reliant on specific individuals reiterating and reenforcing the underlying principles. If we can achieve this, then Splice&#39;s Design System will truly be a success.</p>
<h2>Acknowledgements</h2>
<p>Splice&#39;s design system is built from the contributions and work of many others across the organization. Notable contributors are <a href="https://jayschaul.com">Jay Schaul</a>, <a href="https://twitter.com/madebybourn">Kim Goulbourne</a>, <a href="https://twitter.com/texel">Leigh Caplan</a>, <a href="https://twitter.com/amotion">Andrew Cornett</a>, <a href="https://twitter.com/larawarman">Lara Warman</a>, <a href="https://twitter.com/floridaelago">Florida Elago</a>, <a href="https://twitter.com/jessabean">Jessica Eldredge</a>, <a href="https://twitter.com/potch">Matt Claypotch</a>, <a href="https://twitter.com/deathbearbrown">Sue Lockwood</a>, and more.</p>
]]></content:encoded>
            <enclosure url="https://dandean.com/splice-design-system-splice-design-system" length="0" type="image//splice-design-system-splice-design-system"/>
        </item>
        <item>
            <title><![CDATA[Systems > Tools]]></title>
            <link>https://dandean.com/posts/systems-greater-tools</link>
            <guid isPermaLink="true">https://dandean.com/posts/systems-greater-tools</guid>
            <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>&quot;Our home-rolled CI is too slow, let&#39;s switch to Jenkins!&quot;</p>
</blockquote>
<p>But then Jenkins just runs a bunch of shell scripts outside of source control, build artifacts aren&#39;t taken advantage of, and a single build runs <code>npm install</code> four to six different times.</p>
<blockquote>
<p>&quot;Our users are getting runtime errors – let&#39;s adopt Sentry!&quot;</p>
</blockquote>
<p>But then nobody ever checks Sentry, nobody is accountable for error metrics, and our users continue to get runtime errors.</p>
<blockquote>
<p>&quot;Nobody knows what work is in flight or how long it&#39;s going to take to ship a feature. Let&#39;s switch to [GitHub Projects/Jira] from [Jira/GitHub Projects]!&quot;</p>
</blockquote>
<p>And after the switch teams still can not communicate dependencies or estimate delivery timelines.</p>
<blockquote>
<p>&quot;[Payment provider] is always going down, let&#39;s switch to [other payment provider]!&quot;</p>
</blockquote>
<p>But the new payment provider fails in exciting new ways, and the payment error queue is as bad as it&#39;s always been.</p>
<h2>We often reach for <em>tools</em> to address the failures we encounter when we should start by examining our <em>systems</em>.</h2>
<p>Maybe it isn&#39;t that our CI is too slow, but that it is performing the wrong tasks at the wrong time and needs to be broken apart into a system of contextually-appropriate workflows which support team velocity?</p>
<p>Maybe there are so many runtime errors in production because nobody is accountable for those metrics? If a team&#39;s success takes into account error metrics, then the system of accountability will change how individuals prioritize their time and select their work, leading to trade-offs which will favor fewer runtime errors in production.</p>
<p>It&#39;s easy to go through the motions of agile without understanding how all of the pieces fit together into a system with specific characteristics. Maybe the kanban tool we already have would be more useful if our Project Managers and Lead Engineers went through agile training and actually understood the <em>purpose</em> of all of the rituals, and could see <em>meaning</em> in cumulative flow diagrams?</p>
<p>Maybe our current payment provider is fine, we just have to rethink the system we&#39;ve built around it to be asynchronous, resilient to failure, and automatically retry instead of calling straight through to the third party? Every third-party provider is going to have downtime, and our systems need to be designed with that as an intrinsic characteristic to be managed locally.</p>
<h2>Switching Tools is <em>Expensive</em> 💰💰💰</h2>
<p>The idea of shiny new tools without all of our current problems can be <em>enthralling</em>, but switching tools is often counter-productive.</p>
<p>New tools require time and money to adopt (adjacent tools will need refactoring, integrations will need to be migrated without downtime, people will need training, and at the end artifacts of the previous system will almost inevitably persist), and all too often the failing <em>characteristics of the system</em> remain: build tools are still slow, teams continue to ship runtime errors, delivery timelines are a mystery, and somehow the payment error queue is longer than ever.</p>
<h2>Requirements → System Design → Appropriate Tools</h2>
<style>
  .image-credit {
    font-size: 0.7rem;
    margin-top: -2em;
    text-align: right;
  }
</style>

<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/systems-greater-tools-seattle-subway.png" alt=""></p>
<p class="image-credit">Image by Seattle Subway</p>

<p>I&#39;m not at all saying that switching tools should be avoided, just that <em>tools alone</em> will not magic away the failings of the systems they operate within.</p>
<p>Transportation systems are a good example: a fleet of sleek self-driving electric cars <em>sounds amazing</em>, but they will never solve traffic congestion or get us closer to <a href="https://www.seattle.gov/transportation/projects-and-programs/safety-first/vision-zero">vision zero</a> because Teslas and Volts do not address the failings of transportation systems. To address our transportation system&#39;s failings we need focus on the transportation system itself, much like <a href="https://www.seattlesubway.org/regional-map/">Seattle Subway</a> is doing, and then let that system design guide us in our infrastructure investments.</p>
<p>If our organization&#39;s product delivery system has teams working on features with dependencies across teams, then our kanban tool needs to support linking epics across teams. If our technology stack cannot guarantee that runtime errors are not shipped to production, then our <em>system for accountability</em> must include monitoring and measuring the rate and frequency of these errors, and evaluating our team&#39;s against those metrics as indicators of team and individual success.</p>
<p>We start by understanding our requirements.</p>
<p>Those requirements should lead to a thoughtful system design exploration.</p>
<p>And once we have crafted a system which will support our needs, only then can we select tools which fit into and support that system.</p>
]]></content:encoded>
            <category>system-design</category>
            <category>organizations</category>
            <category>teams</category>
            <enclosure url="https://dandean.com/systems-greater-tools-seattle-subway" length="0" type="image//systems-greater-tools-seattle-subway"/>
        </item>
        <item>
            <title><![CDATA[Building Safe and Stress-Free Automated Releases]]></title>
            <link>https://dandean.com/posts/building-safe-and-stress-free-automated-releases</link>
            <guid isPermaLink="true">https://dandean.com/posts/building-safe-and-stress-free-automated-releases</guid>
            <pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Problem</h2>
<p>Our release process was stressful, time-consuming, and error prone.</p>
<p><strong>This was the process:</strong> <em>notify the team in Slack of your intention to release so nobody else would start their own release, merge your pull request, tag <code>master</code> via GitHub Release, copy the GitHub Release url and paste it into the Customer Relations Slack channel, find the correct &quot;build&quot; job, click through a few screens, click &quot;build&quot; and wait for it to build (about 15 - 25 minutes), deploy the build to dev, find the correct integration test job, trigger it and wait for it to complete (10 - 15 minutes), run integration test failures by QA to make sure they&#39;re not blockers (QA may be busy so you might have to wait until someone has time) ...</em></p>
<p>Are you still with me?</p>
<p><em>...find the production &quot;deploy&quot; job, click through a few screens, select the correct build artifact from the original job, click submit and wait for the deployment to complete, copy the url of the build log and paste it into the GitHub Release, do a smoke test in production, give an &quot;all clear&quot; message to the team so they know they start their own release if they need to.</em></p>
<p>That&#39;s if everything went well. If something went wrong then it would expand and possibly start all over.</p>
<p>This process would run up to three or four times per day by various engineers on the team, and it was error prone. We could easily click the wrong build job link, sending the incorrect build artifact into Production – it happened more than once. The process took forever so engineers would spend half of their day babysitting a release instead of working on product features.</p>
<p>And the team would spend <em>a lot</em> of time to coordinating:</p>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-before.png" alt=""></p>
<h2>Solution</h2>
<p>Automate build, testing, and deployment as a side-effect of source control interactions and build timers. Get humans out of the process of releasing so they can focus on building the product and participating in peer review.</p>
<p><em><strong>The new process:</strong></em> Open a pull request, get peer approval, get QA approval, merge, move on!</p>
<p>Every day at 9AM:</p>
<div class="two-column-list">
- automation fires up and looks for anything merged since the previous release
- release notes are extracted from merged pull requests
- compiled release notes are published to a GitHub Release
- the published GitHub release triggers a build which runs through unit tests, linting, and integration tests, pausing just before the build artifact is sent to production
</div>

<p>When the build is ready, the day&#39;s release marshal is notified and they&#39;re given a link to the &quot;Proceed&quot; screen. The process is async, so if they&#39;re busy they can leave the paused build until their schedule allows. They can hold the build altogether if there are any risky factors (such as a larger ongoing incident). If everything&#39;s in order, they click &quot;Proceed&quot; and get back to work.</p>
<p>Humans are responsible for writing high quality code, reviewing code from their peers, and merging completed work. Automation is responsible for everything else.</p>
<h2>Outcome</h2>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/building-safe-and-stress-free-automated-releases-ci-after.png" alt=""></p>
<ul>
<li>The release process went from a mountain of manual steps to one (clicking &quot;Proceed&quot;)</li>
<li>Time to release went down from 45 minutes to 15 minutes, but humans are only involved for about 3 of those minutes</li>
<li>Removed all opportunities for user-error</li>
<li>Reduced stress</li>
<li>Increased engineering productivity</li>
<li>Changed how other platforms within the organization approach CI</li>
</ul>
<!--
## What I Learned & Skills Improved

* Groovy Programming Language
* Jenkins
* Jenkinsfile declarative pipelines
* Jenkinsfile scripted pipelines
* How to work with Jenkins workspaces as they move across workers
* Strategies for how to ensure complex build workflows do not run into errors
* Integrating with Slack
* Getting a large team comfortable with sweeping workflow changes
-->
]]></content:encoded>
            <category>ci</category>
            <category>automation</category>
            <category>groovy</category>
            <enclosure url="https://dandean.com/building-safe-and-stress-free-automated-releases-jenkins-slack-1" length="0" type="image//building-safe-and-stress-free-automated-releases-jenkins-slack-1"/>
        </item>
        <item>
            <title><![CDATA[Using ESLint to Improve Team Productivity and Happiness]]></title>
            <link>https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</link>
            <guid isPermaLink="true">https://dandean.com/posts/using-eslint-to-improve-team-productivity-and-happiness</guid>
            <pubDate>Sun, 21 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>Problem</h3>
<p>Every engineer contributing to our projects came with a wealth of experience which informed their preferences around code style and formatting. Each arrived at their preferences through trial, error, success, and failure. Unfortunately each arrived at slightly different conclusions. This was not a sustainable long term approach for a growing team.</p>
<p>The organizational problem with allowing divergent style preferences within a team is that it results in less productivity and a lot of avoidable low-level conflict. People change code unrelated to their work, nitpick pull requests, and expend energy on minutia instead of improving the product.</p>
<p>The technical problem is that this behavior introduces avoidable bugs as code is unnecessarily churned while pull requests become difficult to review as much of the patch has no bearing on the feature being built.</p>
<p>The emotional problem is that people get attached to their own ways and don&#39;t want to be forced to follow arbitrary rules which they don&#39;t agree with.</p>
<h3>Solution</h3>
<p>In order to support a large number of engineers contributing to a shared set of projects while maintaining consistent style and formatting we had to lean on automation. But before we could lean on automation we had to agree about the problem.</p>
<p>I opened an issue to discuss the problem, laying out the challenge as I saw them. I proposed that we adopt ESLint configured with an extended<sup><a href="#config-extension">1</a></sup> version of <a href="https://www.npmjs.com/package/eslint-config-airbnb">AirBnB&#39;s ESLint configuration</a> (the extensions would be made to bring it inline with the team&#39;s general preferences). The proposal explicitly stated that there was no de facto &quot;rule decider&quot;<sup><a href="#decider">2</a></sup>, but that anybody could propose modifications. Whether or not to adopt the proposed rule changes would be up to consensus.</p>
<p>There are trade offs. People would have to ensure their code is linted before it can be merged, which is a potential point of frustration. This is an understandable fear: nobody wants errors coming out of nowhere keeping them form getting their work done. The trade off is that what they get out of this compromise is that nobody will nitpick their code – if it passes the linter then style and formatting critique is off the table. A big upside to this is that we can automate the linting work away through editor integration and auto-formatting.</p>
<p>The team agreed. I helped people get their editors configured to automatically surface (and often fix) lint issues. I published <code>@simple/eslint-config</code>, and integrated it into all of our web applications (no small task).</p>
<h3>Outcome</h3>
<p><img src="https://res.cloudinary.com/dandean/image/upload/w_1200/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom.png" alt=""></p>
<p>The outcome has been overwhelmingly positive. Pull Requests no longer get stuck in review for days. There is no back and forth churn of style and formatting as different people work on a file. When linting issues make it into a Pull Request (which is rare, given the widespread use of editor tooling), our CI reports those failures back to the Pull Request before it even gets into peer review.</p>
<h3>Addendum</h3>
<p>The <a href="https://prettier.io/">Prettier</a> auto-formatting tool has come along in the time since we adopted ESLint. I believe we would have used Prettier for much of what we use ESLint for today. We may yet do that, but our current ESLint setup has addressed all of the productivity issues so there&#39;s no pressing need to change tooling yet.</p>
<hr>
<h4>Notes</h4>
<ol>
<li><a name="config-extension"></a> <em>We couldn&#39;t go with the stock version of AirBnB&#39;s configuration for two reasons. The first was that just didn&#39;t agree with some of them. The second was that some rules would have required a level of refactoring which would have had a significant chance of introducing bugs, and in doing so hurting user experience while taking engineering resources away from product development. If the some of the stated goals were increased productivity and happiness, then adopting the default configuration wholesale would have worked against those goals.</em></li>
<li><a name="decider"></a> <em>Why no de facto rule decider? My experience in every organization has been that when someone assumes this role they abuse it and end up holding back the entire team. People get caught up in the dogma of their craft and use their clout to push back on changes which make their rule set irrelevant, such as shifts in the industry away from the technology associated with their expertise.</em></li>
</ol>
]]></content:encoded>
            <category>tooling</category>
            <category>team</category>
            <enclosure url="https://dandean.com/using-eslint-to-improve-team-productivity-and-happiness-eslint-atom" length="0" type="image//using-eslint-to-improve-team-productivity-and-happiness-eslint-atom"/>
        </item>
        <item>
            <title><![CDATA[Rebuilding a Bespoke Web Application for Sustainability]]></title>
            <link>https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</link>
            <guid isPermaLink="true">https://dandean.com/posts/rebuilding-a-bespoke-web-application-for-sustainability</guid>
            <pubDate>Sat, 20 Jan 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>Many Problems</h2>
<p>Our primary banking web application reached a point of unsustainability in that it had intractable architectural problems with no realistic path to improvement.</p>
<p><strong>Client-side performance could not scale.</strong> The application&#39;s client-side performance was sufficient for users in their first couple of years of active use. As their account&#39;s data grew (up into the multiple megabyte range) performance degraded. The application was built with a critical architectural flaw: it assumed the entirety of a user&#39;s account data would be available within the client. Many features relied on this assumption (transaction search, reporting and graphs, transaction sorting, linking to a transaction from support messages, etc), and to break this assumption would require fundamentally re-thinking these features.</p>
<p><strong>Bespoke in-house framework.</strong> The client-side application was built on a bespoke in-house framework similar to Backbone. Unlike Backbone it was undocumented, unmaintained, and had many fundamental problems.</p>
<ul>
<li>&quot;Model&quot; components made no distinction between &quot;collections&quot; and &quot;objects&quot;, and had no schema. This made them confusing to work with as half of the API was irrelevant based on the data structure of its content.</li>
<li>The HTTP transport component was chatty by default, so would fire off unexpected requests constantly. It used attributes of the associated &quot;model&quot; component to infer how to make requests, making it difficult to work with HTTP services which didn&#39;t operate under those same assumptions (which the case more often than not).</li>
<li>&quot;View&quot; components were tightly coupled to &quot;Model&quot; components (a <code>FooView</code> would render a <code>FooModel</code>), making it difficult to build reusable UIs. For example, a transaction list View might contain multiple kinds of transaction-like Models, but would be hardwired to a single Model type. This kind of coupling required complex overrides of core framework logic to work around.</li>
</ul>
<p>e lack of documentation made for a steep learning curve. New engineers would spend weeks running into confusing issues before feeling anywhere near confid