<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Go on Jahvon Dockery</title>
    <link>https://jahvon.dev/tags/go/</link>
    <description>Recent content in Go on Jahvon Dockery</description>
    <generator>Hugo -- 0.148.1</generator>
    <language>en</language>
    <lastBuildDate>Tue, 27 May 2025 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://jahvon.dev/tags/go/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Distributing Work with Go Concurrency</title>
      <link>https://jahvon.dev/notes/distributing-work/</link>
      <pubDate>Tue, 27 May 2025 00:00:00 +0000</pubDate>
      <guid>https://jahvon.dev/notes/distributing-work/</guid>
      <description>&lt;p&gt;A few months back, I worked through VictoriaMetrics&amp;rsquo; &lt;a href=&#34;https://victoriametrics.com/blog/go-sync-mutex/index.html&#34;&gt;Go concurrency series&lt;/a&gt; and wanted to get some practice. So I implemented a few distributed systems, work distribution patterns to see how the concurrency patterns translate.&lt;/p&gt;
&lt;p&gt;Work distribution is fundamental to building scalable systems - you need ways to spread processing across multiple components while coordinating the results. Go&amp;rsquo;s goroutines and channels map well to distributed system concepts - channels as service communication, goroutines as system components, WaitGroups for coordination. Here&amp;rsquo;s what I learned.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>A few months back, I worked through VictoriaMetrics&rsquo; <a href="https://victoriametrics.com/blog/go-sync-mutex/index.html">Go concurrency series</a> and wanted to get some practice. So I implemented a few distributed systems, work distribution patterns to see how the concurrency patterns translate.</p>
<p>Work distribution is fundamental to building scalable systems - you need ways to spread processing across multiple components while coordinating the results. Go&rsquo;s goroutines and channels map well to distributed system concepts - channels as service communication, goroutines as system components, WaitGroups for coordination. Here&rsquo;s what I learned.</p>
<h2 id="producer-consumer-async-work-distribution">Producer-Consumer: Async Work Distribution</h2>
<p>Producers generate work and send it through channels while consumers process it asynchronously.</p>
<div class="highlight"><pre tabindex="0" style="color:#d6cbb4;background-color:#252b2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#e67e80">type</span> ConsumerResult <span style="color:#e67e80">struct</span> {
</span></span><span style="display:flex;"><span>    ConsumerID <span style="color:#dbbc7f">int</span>
</span></span><span style="display:flex;"><span>    Data       <span style="color:#dbbc7f">string</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#859289;font-style:italic">// start multiple consumers</span>
</span></span><span style="display:flex;"><span><span style="color:#e67e80">for</span> id <span style="color:#7a8478">:=</span> <span style="color:#d699b6">0</span>; id &lt; numConsumers; id<span style="color:#7a8478">++</span> {
</span></span><span style="display:flex;"><span>    wg.<span style="color:#b2c98f">Add</span>(<span style="color:#d699b6">1</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#e67e80">go</span> <span style="color:#e67e80">func</span>(consumerID <span style="color:#dbbc7f">int</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#e67e80">defer</span> wg.<span style="color:#b2c98f">Done</span>()
</span></span><span style="display:flex;"><span>        <span style="color:#e67e80">for</span> msg <span style="color:#7a8478">:=</span> <span style="color:#e67e80">range</span> msgChan {
</span></span><span style="display:flex;"><span>            result <span style="color:#7a8478">:=</span> ConsumerResult{
</span></span><span style="display:flex;"><span>                ConsumerID: consumerID,
</span></span><span style="display:flex;"><span>                Data: fmt.<span style="color:#b2c98f">Sprintf</span>(<span style="color:#b2c98f">&#34;processed-%d&#34;</span>, msg),
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>            resultChan <span style="color:#7a8478">&lt;-</span> result
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }(id)
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#859289;font-style:italic">// start a single producer that sends work into a channel</span>
</span></span><span style="display:flex;"><span><span style="color:#e67e80">go</span> <span style="color:#e67e80">func</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#e67e80">defer</span> <span style="color:#d699b6">close</span>(msgChan)
</span></span><span style="display:flex;"><span>    <span style="color:#e67e80">for</span> i <span style="color:#7a8478">:=</span> <span style="color:#d699b6">1</span>; i <span style="color:#7a8478">&lt;=</span> <span style="color:#d699b6">25</span>; i<span style="color:#7a8478">++</span> {
</span></span><span style="display:flex;"><span>        msgChan <span style="color:#7a8478">&lt;-</span> i
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}()
</span></span></code></pre></div><p>Buffered channels give you throttling - if consumers can&rsquo;t keep up, the producer blocks instead of consuming memory.</p>
<p>Use this pattern for event streaming, async processing, or decoupling generation speed from processing speed. It maps directly to Kafka or microservice event handling.</p>
<h2 id="worker-pools-controlled-work-distribution">Worker Pools: Controlled Work Distribution</h2>
<p>Worker pools give you structure - fixed number of workers pulling from the same job queue. It&rsquo;s like running N service instances behind a load balancer.</p>
<div class="highlight"><pre tabindex="0" style="color:#d6cbb4;background-color:#252b2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#e67e80">type</span> PoolJob <span style="color:#e67e80">struct</span> {
</span></span><span style="display:flex;"><span>    ID   <span style="color:#dbbc7f">int</span>
</span></span><span style="display:flex;"><span>    Data <span style="color:#dbbc7f">string</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#859289;font-style:italic">// start a fixed number of workers</span>
</span></span><span style="display:flex;"><span><span style="color:#e67e80">for</span> i <span style="color:#7a8478">:=</span> <span style="color:#d699b6">0</span>; i &lt; numWorkers; i<span style="color:#7a8478">++</span> {
</span></span><span style="display:flex;"><span>    wg.<span style="color:#b2c98f">Add</span>(<span style="color:#d699b6">1</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#e67e80">go</span> <span style="color:#e67e80">func</span>(workerID <span style="color:#dbbc7f">int</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#e67e80">defer</span> wg.<span style="color:#b2c98f">Done</span>()
</span></span><span style="display:flex;"><span>        <span style="color:#e67e80">for</span> job <span style="color:#7a8478">:=</span> <span style="color:#e67e80">range</span> jobChan {
</span></span><span style="display:flex;"><span>            <span style="color:#859289;font-style:italic">// do some work</span>
</span></span><span style="display:flex;"><span>            time.<span style="color:#b2c98f">Sleep</span>(<span style="color:#d699b6">100</span> <span style="color:#7a8478">*</span> time.Millisecond)
</span></span><span style="display:flex;"><span>            results <span style="color:#7a8478">&lt;-</span> PoolResult{
</span></span><span style="display:flex;"><span>                WorkerID: workerID,
</span></span><span style="display:flex;"><span>                JobID:    job.ID,
</span></span><span style="display:flex;"><span>                Value:    fmt.<span style="color:#b2c98f">Sprintf</span>(<span style="color:#b2c98f">&#34;processed-%s&#34;</span>, job.Data),
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }(i)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The job channel acts like a load balancer - work goes to whichever worker is available.</p>
<p>This pattern is good for CPU-heavy tasks or when you need predictable resource usage. It&rsquo;s similar to scaling microservice instances for ingress traffic.</p>
<h2 id="batch-processing-efficient-work-distribution">Batch Processing: Efficient Work Distribution</h2>
<p>Sometimes you need to group items into batches for efficiency or to respect downstream rate limits. This example handles batching by size and by time.</p>
<div class="highlight"><pre tabindex="0" style="color:#d6cbb4;background-color:#252b2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#e67e80">func</span> (p <span style="color:#7a8478">*</span>BatchProcessor) <span style="color:#b2c98f">startBatchAggregator</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#e67e80">go</span> <span style="color:#e67e80">func</span>() {
</span></span><span style="display:flex;"><span>        batch <span style="color:#7a8478">:=</span> <span style="color:#d699b6">make</span>([]<span style="color:#dbbc7f">int</span>, <span style="color:#d699b6">0</span>, p.batchSize)
</span></span><span style="display:flex;"><span>        flushTimer <span style="color:#7a8478">:=</span> time.<span style="color:#b2c98f">NewTimer</span>(<span style="color:#d699b6">2</span> <span style="color:#7a8478">*</span> time.Second)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        sendBatch <span style="color:#7a8478">:=</span> <span style="color:#e67e80">func</span>() {
</span></span><span style="display:flex;"><span>            <span style="color:#7a8478">&lt;-</span>p.rateLimiter.C <span style="color:#859289;font-style:italic">// wait for rate limiter</span>
</span></span><span style="display:flex;"><span>            batchCopy <span style="color:#7a8478">:=</span> <span style="color:#d699b6">make</span>([]<span style="color:#dbbc7f">int</span>, <span style="color:#d699b6">len</span>(batch))
</span></span><span style="display:flex;"><span>            <span style="color:#d699b6">copy</span>(batchCopy, batch)
</span></span><span style="display:flex;"><span>            p.batchChan <span style="color:#7a8478">&lt;-</span> batchCopy
</span></span><span style="display:flex;"><span>            batch = batch[:<span style="color:#d699b6">0</span>]
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#e67e80">for</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#e67e80">select</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#e67e80">case</span> item, ok <span style="color:#7a8478">:=</span> <span style="color:#7a8478">&lt;-</span>p.itemChan:
</span></span><span style="display:flex;"><span>                <span style="color:#e67e80">if</span> !ok {
</span></span><span style="display:flex;"><span>                    <span style="color:#e67e80">if</span> <span style="color:#d699b6">len</span>(batch) &gt; <span style="color:#d699b6">0</span> {
</span></span><span style="display:flex;"><span>                        <span style="color:#b2c98f">sendBatch</span>()
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>                    <span style="color:#d699b6">close</span>(p.batchChan)
</span></span><span style="display:flex;"><span>                    <span style="color:#e67e80">return</span>
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                batch = <span style="color:#d699b6">append</span>(batch, item)
</span></span><span style="display:flex;"><span>                <span style="color:#e67e80">if</span> <span style="color:#d699b6">len</span>(batch) <span style="color:#7a8478">&gt;=</span> p.batchSize {
</span></span><span style="display:flex;"><span>                    <span style="color:#b2c98f">sendBatch</span>()
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#e67e80">case</span> <span style="color:#7a8478">&lt;-</span>flushTimer.C:
</span></span><span style="display:flex;"><span>                <span style="color:#e67e80">if</span> <span style="color:#d699b6">len</span>(batch) &gt; <span style="color:#d699b6">0</span> {
</span></span><span style="display:flex;"><span>                    <span style="color:#b2c98f">sendBatch</span>()
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }()
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The <code>select</code> with the flush timer gives you batches when they&rsquo;re full OR when time runs out. The rate limiter prevents overwhelming the batch processor and its external dependencies. I used a simple timer here, but you can replace it with much more sophisticated limiting logic as needed.</p>
<p>The batch processing pattern works well for database bulk operations, API integrations with rate limits, or protecting downstream services.</p>
<h2 id="a-few-notes">A Few Notes</h2>
<p>Working through these patterns reinforced a few things:</p>
<ul>
<li>Channels behave like message queues with capacity limits and natural flow control.</li>
<li>Multiple goroutines running the same function is basically horizontal scaling - same patterns you&rsquo;d use for scaling system components.</li>
<li>These patterns compose well. Producer-consumer provides the foundation, worker pools add structure, batching adds efficiency.</li>
</ul>
<table>
  <thead>
      <tr>
          <th>Pattern</th>
          <th>Analogy</th>
          <th>Use Case</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Producer-Consumer</td>
          <td>Message queues, event streams</td>
          <td>Event-driven architectures, async processing</td>
      </tr>
      <tr>
          <td>Worker Pools</td>
          <td>Load-balanced system components</td>
          <td>Controlled concurrency, predictable resources</td>
      </tr>
      <tr>
          <td>Batch Processing</td>
          <td>ETL pipelines, bulk APIs</td>
          <td>Rate limiting, bulk operations</td>
      </tr>
  </tbody>
</table>
<h3 id="error-handling">Error Handling</h3>
<p>Error handling in concurrent code needs to be explicit and planned upfront, similar to how distributed systems need circuit breakers and retry logic. I used result structs that carry either data or errors:</p>
<div class="highlight"><pre tabindex="0" style="color:#d6cbb4;background-color:#252b2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#e67e80">type</span> WorkResult <span style="color:#e67e80">struct</span> {
</span></span><span style="display:flex;"><span>    Data <span style="color:#dbbc7f">string</span>
</span></span><span style="display:flex;"><span>    Err  <span style="color:#dbbc7f">error</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#e67e80">func</span> <span style="color:#b2c98f">worker</span>(jobs <span style="color:#7a8478">&lt;-</span><span style="color:#e67e80">chan</span> <span style="color:#dbbc7f">int</span>, results <span style="color:#e67e80">chan</span><span style="color:#7a8478">&lt;-</span> WorkResult) {
</span></span><span style="display:flex;"><span>    <span style="color:#e67e80">for</span> job <span style="color:#7a8478">:=</span> <span style="color:#e67e80">range</span> jobs {
</span></span><span style="display:flex;"><span>        <span style="color:#e67e80">if</span> job<span style="color:#7a8478">%</span><span style="color:#d699b6">7</span> <span style="color:#7a8478">==</span> <span style="color:#d699b6">0</span> { <span style="color:#859289;font-style:italic">// simulate some failures</span>
</span></span><span style="display:flex;"><span>            results <span style="color:#7a8478">&lt;-</span> WorkResult{Err: fmt.<span style="color:#b2c98f">Errorf</span>(<span style="color:#b2c98f">&#34;job %d failed&#34;</span>, job)}
</span></span><span style="display:flex;"><span>            <span style="color:#e67e80">continue</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        results <span style="color:#7a8478">&lt;-</span> WorkResult{Data: fmt.<span style="color:#b2c98f">Sprintf</span>(<span style="color:#b2c98f">&#34;processed-%d&#34;</span>, job)}
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>For timeouts and cancellation, <code>context.Context</code> works well:</p>
<div class="highlight"><pre tabindex="0" style="color:#d6cbb4;background-color:#252b2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#e67e80">func</span> <span style="color:#b2c98f">workerWithTimeout</span>(ctx context.Context, jobs <span style="color:#7a8478">&lt;-</span><span style="color:#e67e80">chan</span> <span style="color:#dbbc7f">int</span>, results <span style="color:#e67e80">chan</span><span style="color:#7a8478">&lt;-</span> WorkResult) {
</span></span><span style="display:flex;"><span>    <span style="color:#e67e80">for</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#e67e80">select</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#e67e80">case</span> job <span style="color:#7a8478">:=</span> <span style="color:#7a8478">&lt;-</span>jobs:
</span></span><span style="display:flex;"><span>            <span style="color:#859289;font-style:italic">// process the job</span>
</span></span><span style="display:flex;"><span>        <span style="color:#e67e80">case</span> <span style="color:#7a8478">&lt;-</span>ctx.<span style="color:#b2c98f">Done</span>():
</span></span><span style="display:flex;"><span>            results <span style="color:#7a8478">&lt;-</span> WorkResult{Err: ctx.<span style="color:#b2c98f">Err</span>()}
</span></span><span style="display:flex;"><span>            <span style="color:#e67e80">return</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="beyond-the-basics">Beyond the basics</h3>
<p>These patterns scratch the surface of Go&rsquo;s concurrency toolkit. The VictoriaMetrics series I mentioned dives deep into more advanced primitives like <code>sync.Mutex</code> for protecting shared state, <code>sync.Pool</code> for object reuse, <code>sync.Once</code> for one-time initialization, and <code>sync.Map</code> for concurrent map access. I recommend checking it out if you haven&rsquo;t already!</p>
<p>I intentionally stuck to channels and WaitGroups in my examples here - they mirror message passing between services naturally and keep the code readable. Once those patterns are solid, adding mutexes and other synchronization primitives becomes intuitive because you already understand the coordination challenges.</p>
<p>As you build more complex systems, you&rsquo;ll need these other tools. Mutexes become your distributed locks, sync.Pool mirrors connection pooling in microservices, sync.Once handles singleton initialization across service instances (similar to leader election), and sync.Map acts like shared caches that multiple services access concurrently.</p>
<p><strong><a href="https://github.com/jahvon/go-concurrency-examples">Full code examples on GitHub</a></strong></p>
<p><em>Next: circuit breaker and fan-in/fan-out implementations.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Forging flow: My Journey of Creation and Learning</title>
      <link>https://jahvon.dev/notes/forging-flow/</link>
      <pubDate>Sat, 08 Feb 2025 00:00:00 +0000</pubDate>
      <guid>https://jahvon.dev/notes/forging-flow/</guid>
      <description>&lt;p&gt;Over the last couple of years, I have been having fun experimenting with ways to streamline my developer experience. This may largely stem from my Developer Experience (DevX) focus as a software / platform engineer in the CarGurus DevX organization. (&lt;em&gt;side note: Check out &lt;a href=&#34;https://www.cargurus.dev/How-CarGurus-is-supercharging-our-microservice-developer-experience/&#34;&gt;this blog post&lt;/a&gt; I wrote on how we supercharged the experience for our Product Engineers&lt;/em&gt;)&lt;/p&gt;
&lt;p&gt;However, the challenges that &lt;em&gt;I&lt;/em&gt; face at work and on my side projects aren&amp;rsquo;t quite the same as the ones faced by CarGurus product engineers. I started to find myself drowning in a sea of scattered commands, scripts, and tools. This, combined with my desire to find opportunities to learn more about Go patterns and libraries in a low-risk way, motivated me to invest some free time developing an &amp;ldquo;integrated development platform&amp;rdquo; - or at least the foundations for one!&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Over the last couple of years, I have been having fun experimenting with ways to streamline my developer experience. This may largely stem from my Developer Experience (DevX) focus as a software / platform engineer in the CarGurus DevX organization. (<em>side note: Check out <a href="https://www.cargurus.dev/How-CarGurus-is-supercharging-our-microservice-developer-experience/">this blog post</a> I wrote on how we supercharged the experience for our Product Engineers</em>)</p>
<p>However, the challenges that <em>I</em> face at work and on my side projects aren&rsquo;t quite the same as the ones faced by CarGurus product engineers. I started to find myself drowning in a sea of scattered commands, scripts, and tools. This, combined with my desire to find opportunities to learn more about Go patterns and libraries in a low-risk way, motivated me to invest some free time developing an &ldquo;integrated development platform&rdquo; - or at least the foundations for one!</p>
<p>Enter flow: my open-source task runner and workflow automation tool. What started as a simple itch to scratch has evolved into the foundations of a comprehensive platform for wrangling dev workflows across projects. Looking back, I realize it would have been easier to just migrate everything into a tool like <a href="https://taskfile.dev/">Taskfile</a> or <a href="https://just.systems/">Just</a>, but my vision for my own personal platform doesn&rsquo;t stop at the CLI. Taking that route also would have left my learning desires unmet. While much of my influence for flow comes from cloud-native projects and ideals, I&rsquo;ve approached it from a local-first perspective - one where repeatable &ldquo;micro-workflows&rdquo; can be pieced together however <em>you</em> desire; making them easily discoverable, automated, and observable.</p>
<p>It&rsquo;s ambitious, but that&rsquo;s what excites me! There&rsquo;s so much experimenting and learning ahead. This is my first blog post, but if this interests you, please return! I&rsquo;ll be using it to document my learnings and progress on this project and some of my other side projects.</p>
<h2 id="building-the-foundation">Building the Foundation</h2>
<p>My first build of flow centered around two simple concepts driven by YAML files:</p>
<ul>
<li>Workspaces for organizing tasks across projects/repos</li>
<li>Executables for defining those tasks</li>
</ul>
<p>I included a simple <a href="https://github.com/rivo/tview/">tview</a> terminal UI implementation to simplify the discovery of workspaces and executables across my system. While I&rsquo;ve since moved away from that library, it helped me conceptualize much of the current TUI.</p>
<p>Through usage, I found myself iterating <em>a lot</em>. As I onboarded more workspaces and as those workspaces grew in complexity, flow&rsquo;s feature set had to grow. My favorite components to implement have been the <a href="https://github.com/charmbracelet/bubbletea">bubbletea</a> TUI framework, an internal documentation generator for the <a href="https://flowexec.io/">flowexec.io</a> site, the <a href="https://flowexec.io/#/guide/templating">templating</a> workflow, and the <a href="https://flowexec.io/#/guide/state">state</a> and <a href="https://flowexec.io/#/guide/conditional">conditional</a> management of  serial and parallel executable types. &rsquo;ll dive deeper into those in follow-up posts, but I invite you to explore the guides at flowexec.io for a complete overview of where flow stands today.</p>
<p>At it&rsquo;s core, the flow CLI is a YAML-driven task runner. Here is an example of a flow file that I have for my Authentik server deployed in my home cluster; it uses the <code>exec</code> executable type to define the command that&rsquo;s run:</p>
<div class="highlight"><pre tabindex="0" style="color:#d6cbb4;background-color:#252b2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#7a8478">namespace</span>: authentik <span style="color:#859289;font-style:italic"># optional, additional grouping in a workspace</span>
</span></span><span style="display:flex;"><span><span style="color:#7a8478">tags</span>: [k8s, auth] <span style="color:#859289;font-style:italic"># useful for filtering the `flow library` command</span>
</span></span><span style="display:flex;"><span><span style="color:#859289;font-style:italic"># this description is rendered as markdown (alongside other executable info)</span>
</span></span><span style="display:flex;"><span><span style="color:#859289;font-style:italic"># when viewing in the `flow library`.</span>
</span></span><span style="display:flex;"><span><span style="color:#7a8478">description</span>: |<span style="color:#b2c98f">
</span></span></span><span style="display:flex;"><span><span style="color:#b2c98f">  **References:**
</span></span></span><span style="display:flex;"><span><span style="color:#b2c98f">  - https://goauthentik.io/
</span></span></span><span style="display:flex;"><span><span style="color:#b2c98f">  - https://github.com/goauthentik/helm</span>
</span></span><span style="display:flex;"><span><span style="color:#7a8478">executables</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#859289;font-style:italic"># flow install authentik:app</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#7a8478">verb</span>: install
</span></span><span style="display:flex;"><span>    <span style="color:#7a8478">name</span>: app
</span></span><span style="display:flex;"><span>    <span style="color:#7a8478">aliases</span>: [chart]
</span></span><span style="display:flex;"><span>    <span style="color:#7a8478">description</span>: Upgrade/install Authentik Helm chart
</span></span><span style="display:flex;"><span>    <span style="color:#7a8478">exec</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#7a8478">params</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#859289;font-style:italic"># secrets are managed with the integrated vault via the `flow secret` command</span>
</span></span><span style="display:flex;"><span>        - <span style="color:#7a8478">secretRef</span>: authentik-secret-key
</span></span><span style="display:flex;"><span>          <span style="color:#7a8478">envKey</span>: AUTHENTIK_SECRET_KEY
</span></span><span style="display:flex;"><span>        - <span style="color:#7a8478">secretRef</span>: authentik-db-password
</span></span><span style="display:flex;"><span>          <span style="color:#7a8478">envKey</span>: AUTHENTIK_DB_PASSWORD
</span></span><span style="display:flex;"><span>      <span style="color:#7a8478">cmd</span>: |<span style="color:#b2c98f">
</span></span></span><span style="display:flex;"><span><span style="color:#b2c98f">        helm upgrade --install authentik authentik/authentik \
</span></span></span><span style="display:flex;"><span><span style="color:#b2c98f">          --version 2024.10.4 \
</span></span></span><span style="display:flex;"><span><span style="color:#b2c98f">          --namespace auth --create-namespace \
</span></span></span><span style="display:flex;"><span><span style="color:#b2c98f">          --set authentik.secret_key=$AUTHENTIK_SECRET_KEY \
</span></span></span><span style="display:flex;"><span><span style="color:#b2c98f">          --set authentik.postgresql.password=$AUTHENTIK_DB_PASSWORD \
</span></span></span><span style="display:flex;"><span><span style="color:#b2c98f">          --set postgresql.auth.password=$AUTHENTIK_DB_PASSWORD \
</span></span></span><span style="display:flex;"><span><span style="color:#b2c98f">          --set postgresql.postgresqlPassword=$AUTHENTIK_DB_PASSWORD \
</span></span></span><span style="display:flex;"><span><span style="color:#b2c98f">          -f values.yaml</span>
</span></span></code></pre></div><p>The flow CLI provides a consistent experience for all executable runs and searches. This includes automatically generating a summary markdown document viewable with the <code>flow library</code> command, log formatting and archiving, and a configurable TUI experience.</p>
<p>This will show up in the <code>flow library</code> as rendered markdown:</p>
<p><img alt="Authentik Executable" loading="lazy" src="/images/library-authentik-exec.png"></p>
<p>As my needs evolved, I added more executable configurations and types. Here&rsquo;s an example of a common <code>request</code> executable I use to pause my home&rsquo;s pi.hole blocking:</p>
<div class="highlight"><pre tabindex="0" style="color:#d6cbb4;background-color:#252b2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#7a8478">executables</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#859289;font-style:italic"># flow pause pihole</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#7a8478">verb</span>: pause
</span></span><span style="display:flex;"><span>    <span style="color:#7a8478">name</span>: pihole
</span></span><span style="display:flex;"><span>    <span style="color:#7a8478">request</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#7a8478">method</span>: <span style="color:#b2c98f">&#34;POST&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#7a8478">args</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#7a8478">pos</span>: <span style="color:#d699b6">1</span>
</span></span><span style="display:flex;"><span>          <span style="color:#7a8478">envKey</span>: DURATION
</span></span><span style="display:flex;"><span>          <span style="color:#7a8478">default</span>: <span style="color:#d699b6">300</span>
</span></span><span style="display:flex;"><span>          <span style="color:#7a8478">type</span>: int
</span></span><span style="display:flex;"><span>      <span style="color:#7a8478">params</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#7a8478">secretRef</span>: pihole-pwhash
</span></span><span style="display:flex;"><span>          <span style="color:#7a8478">envKey</span>: PWHASH
</span></span><span style="display:flex;"><span>      <span style="color:#7a8478">url</span>: http://pi.hole/admin/api.php?disable=$DURATION&amp;auth=$PWHASH
</span></span><span style="display:flex;"><span>      <span style="color:#7a8478">validStatusCodes</span>: [<span style="color:#d699b6">200</span>]
</span></span><span style="display:flex;"><span>      <span style="color:#7a8478">logResponse</span>: <span style="color:#e67e80">true</span>
</span></span><span style="display:flex;"><span>      <span style="color:#7a8478">transformResponse</span>: if .status == &#34;disabled&#34; then .status = &#34;paused&#34; else . end
</span></span></code></pre></div><p><img alt="PiHole Executable" loading="lazy" src="/images/library-pihole-exec.png"></p>
<p>Here&rsquo;s an example of the log output:</p>
<p><img alt="PiHole Executable Logs" loading="lazy" src="/images/log-pihole-exec.png"></p>
<h2 id="lessons-learned">Lessons Learned</h2>
<p>Building flow has been an incredible opportunity to deepen my understanding of Go and its ecosystem. Here are a few key lessons that have significantly shaped how I write Go now:</p>
<h3 id="small-packages-and-interfaces-made-testing-a-breeze">Small Packages and Interfaces Made Testing a Breeze</h3>
<p>This approach makes testing easier, improves code organization, and makes refactoring as ideas evolve a joy.</p>
<p>Each executable type has its own Runner, making it simple to extend the system with new types. Here&rsquo;s a snippet that demonstrates the ease of using this type when assigning an executable to a <code>Runner</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#d6cbb4;background-color:#252b2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#859289;font-style:italic">//go:generate mockgen -destination=mocks/mock_runner.go -package=mocks github.com/jahvon/flow/internal/runner Runner</span>
</span></span><span style="display:flex;"><span><span style="color:#e67e80">type</span> Runner <span style="color:#e67e80">interface</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#b2c98f">Name</span>() <span style="color:#dbbc7f">string</span>
</span></span><span style="display:flex;"><span>    <span style="color:#b2c98f">Exec</span>(ctx <span style="color:#7a8478">*</span>context.Context, e <span style="color:#7a8478">*</span>executable.Executable, eng engine.Engine, inputEnv <span style="color:#e67e80">map</span>[<span style="color:#dbbc7f">string</span>]<span style="color:#dbbc7f">string</span>) <span style="color:#dbbc7f">error</span>
</span></span><span style="display:flex;"><span>    <span style="color:#b2c98f">IsCompatible</span>(executable <span style="color:#7a8478">*</span>executable.Executable) <span style="color:#dbbc7f">bool</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This interface allows me to easily add new executable types by just implementing these three methods. The core execution logic remains clean and extensible:</p>
<div class="highlight"><pre tabindex="0" style="color:#d6cbb4;background-color:#252b2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#e67e80">func</span> <span style="color:#b2c98f">Exec</span>(
</span></span><span style="display:flex;"><span>    ctx <span style="color:#7a8478">*</span>context.Context,
</span></span><span style="display:flex;"><span>    executable <span style="color:#7a8478">*</span>executable.Executable,
</span></span><span style="display:flex;"><span>    eng engine.Engine,
</span></span><span style="display:flex;"><span>    inputEnv <span style="color:#e67e80">map</span>[<span style="color:#dbbc7f">string</span>]<span style="color:#dbbc7f">string</span>,
</span></span><span style="display:flex;"><span>) <span style="color:#dbbc7f">error</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#e67e80">var</span> assignedRunner Runner
</span></span><span style="display:flex;"><span>    <span style="color:#e67e80">for</span> _, runner <span style="color:#7a8478">:=</span> <span style="color:#e67e80">range</span> registeredRunners {
</span></span><span style="display:flex;"><span>       <span style="color:#e67e80">if</span> runner.<span style="color:#b2c98f">IsCompatible</span>(executable) {
</span></span><span style="display:flex;"><span>          assignedRunner = runner
</span></span><span style="display:flex;"><span>          <span style="color:#e67e80">break</span>
</span></span><span style="display:flex;"><span>       }
</span></span><span style="display:flex;"><span>    }    <span style="color:#e67e80">if</span> assignedRunner <span style="color:#7a8478">==</span> <span style="color:#e67e80">nil</span> {
</span></span><span style="display:flex;"><span>       <span style="color:#e67e80">return</span> fmt.<span style="color:#b2c98f">Errorf</span>(<span style="color:#b2c98f">&#34;compatible runner not found for executable %s&#34;</span>, executable.<span style="color:#b2c98f">ID</span>())
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#e67e80">if</span> executable.Timeout <span style="color:#7a8478">==</span> <span style="color:#d699b6">0</span> {
</span></span><span style="display:flex;"><span>       <span style="color:#e67e80">return</span> assignedRunner.<span style="color:#b2c98f">Exec</span>(ctx, executable, eng, inputEnv)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    done <span style="color:#7a8478">:=</span> <span style="color:#d699b6">make</span>(<span style="color:#e67e80">chan</span> <span style="color:#dbbc7f">error</span>, <span style="color:#d699b6">1</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#e67e80">go</span> <span style="color:#e67e80">func</span>() {
</span></span><span style="display:flex;"><span>       done <span style="color:#7a8478">&lt;-</span> assignedRunner.<span style="color:#b2c98f">Exec</span>(ctx, executable, eng, inputEnv)
</span></span><span style="display:flex;"><span>    }()
</span></span><span style="display:flex;"><span>    <span style="color:#e67e80">select</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#e67e80">case</span> err <span style="color:#7a8478">:=</span> <span style="color:#7a8478">&lt;-</span>done:
</span></span><span style="display:flex;"><span>       <span style="color:#e67e80">return</span> err
</span></span><span style="display:flex;"><span>    <span style="color:#e67e80">case</span> <span style="color:#7a8478">&lt;-</span>time.<span style="color:#b2c98f">After</span>(executable.Timeout):
</span></span><span style="display:flex;"><span>       <span style="color:#e67e80">return</span> fmt.<span style="color:#b2c98f">Errorf</span>(<span style="color:#b2c98f">&#34;timeout after %v&#34;</span>, executable.Timeout)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>For testing, I use <a href="https://onsi.github.io/ginkgo/">ginkgo</a> for its expressive BDD-style syntax and <a href="https://github.com/uber-go/mock">GoMock</a> to generate a mock runner. This mock simulates serial and parallel execution without the complexity of managing real subprocesses or network calls. This approach has been invaluable for verifying complex concurrent behaviors, especially when testing features like timeout handling, parallel execution limits, and failure modes in a reliable, repeatable way.</p>
<h3 id="build-better-abstractions-with-service-layers">Build Better Abstractions with Service Layers</h3>
<p>When working with third-party modules or I/O components, wrapping your interaction with a service layer is invaluable. It keeps business logic decoupled from implementation details and simplifies testing and refactoring. In flow, I use this pattern extensively for components like shell operations, file system operations, and process management.</p>
<p>For example, my run service abstracts away the complexities of running shell operations with the <a href="https://github.com/mvdan/sh">github.com/mvdan/sh</a> library.  This means if I need to change how shell commands are executed or add new shell features, I only need to update the service implementation, not the core application logic. You can explore some of my service implementations in the <a href="https://github.com/jahvon/flow/tree/main/internal/services">source code</a>.</p>
<h3 id="good-tools-are-worth-the-investment">Good Tools Are Worth the Investment</h3>
<p>Investing in custom tooling or incoproating open source, especially for patterns like code generation, can significantly streamline your development workflow and reduce boilerplate. In flow, I define all types in YAML and use <a href="https://github.com/atombender/go-jsonschema">go-jsonschema</a> for <code>codegen</code>.</p>
<p>Here&rsquo;s an example of how my <code>Launch</code> executable type is defined:</p>
<div class="highlight"><pre tabindex="0" style="color:#d6cbb4;background-color:#252b2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#7a8478">LaunchExecutableType</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#7a8478">type</span>: object
</span></span><span style="display:flex;"><span>  <span style="color:#7a8478">required</span>: [uri]
</span></span><span style="display:flex;"><span>  <span style="color:#7a8478">description</span>: Launches an application or opens a URI.
</span></span><span style="display:flex;"><span>  <span style="color:#7a8478">properties</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#7a8478">params</span>:
</span></span><span style="display:flex;"><span>	  <span style="color:#7a8478">$ref</span>: <span style="color:#b2c98f">&#39;#/definitions/ParameterList&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#7a8478">args</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#7a8478">$ref</span>: <span style="color:#b2c98f">&#39;#/definitions/ArgumentList&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#7a8478">app</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#7a8478">type</span>: string
</span></span><span style="display:flex;"><span>      <span style="color:#7a8478">description</span>: The application to launch the URI with.
</span></span><span style="display:flex;"><span>      <span style="color:#7a8478">default</span>: <span style="color:#b2c98f">&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#7a8478">uri</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#7a8478">type</span>: string
</span></span><span style="display:flex;"><span>      <span style="color:#7a8478">description</span>: The URI to launch. This can be a file path or a web URL.
</span></span><span style="display:flex;"><span>      <span style="color:#7a8478">default</span>: <span style="color:#b2c98f">&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#7a8478">wait</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#7a8478">type</span>: boolean
</span></span><span style="display:flex;"><span>      <span style="color:#7a8478">description</span>: If set to true, the executable will wait for the launched application to exit before continuing.
</span></span><span style="display:flex;"><span>      <span style="color:#7a8478">default</span>: <span style="color:#e67e80">false</span>
</span></span></code></pre></div><p>This generates both the Go type and its documentation:</p>
<div class="highlight"><pre tabindex="0" style="color:#d6cbb4;background-color:#252b2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#859289;font-style:italic">// Launches an application or opens a URI.</span>
</span></span><span style="display:flex;"><span><span style="color:#e67e80">type</span> LaunchExecutableType <span style="color:#e67e80">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#859289;font-style:italic">// The application to launch the URI with.</span>
</span></span><span style="display:flex;"><span>	App <span style="color:#dbbc7f">string</span> <span style="color:#b2c98f">`json:&#34;app,omitempty&#34; yaml:&#34;app,omitempty&#34; mapstructure:&#34;app,omitempty&#34;`</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#859289;font-style:italic">// Args corresponds to the JSON schema field &#34;args&#34;.</span>
</span></span><span style="display:flex;"><span>	Args ArgumentList <span style="color:#b2c98f">`json:&#34;args,omitempty&#34; yaml:&#34;args,omitempty&#34; mapstructure:&#34;args,omitempty&#34;`</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#859289;font-style:italic">// Params corresponds to the JSON schema field &#34;params&#34;.</span>
</span></span><span style="display:flex;"><span>	Params ParameterList <span style="color:#b2c98f">`json:&#34;params,omitempty&#34; yaml:&#34;params,omitempty&#34; mapstructure:&#34;params,omitempty&#34;`</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#859289;font-style:italic">// The URI to launch. This can be a file path or a web URL.</span>
</span></span><span style="display:flex;"><span>	URI <span style="color:#dbbc7f">string</span> <span style="color:#b2c98f">`json:&#34;uri&#34; yaml:&#34;uri&#34; mapstructure:&#34;uri&#34;`</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#859289;font-style:italic">// If set to true, the executable will wait for the launched application to exit</span>
</span></span><span style="display:flex;"><span>	<span style="color:#859289;font-style:italic">// before continuing.</span>
</span></span><span style="display:flex;"><span>	Wait <span style="color:#dbbc7f">bool</span> <span style="color:#b2c98f">`json:&#34;wait,omitempty&#34; yaml:&#34;wait,omitempty&#34; mapstructure:&#34;wait,omitempty&#34;`</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>I&rsquo;ve also built a <code>docsgen</code> tool that uses this same schema to generate structured documentation. This means my types, code, and documentation all stay in sync automatically. You can see the generated type documentation for Launch <a href="https://flowexec.io/#/types/flowfile?id=executablelaunchexecutabletype">here</a>.</p>
<p>This investment in tooling has paid off repeatedly, especially as flow&rsquo;s type system has grown more complex. It reduces errors, ensures consistency, and lets me focus on implementing features rather than maintaining boilerplate code.</p>
<h2 id="the-road-ahead">The Road Ahead</h2>
<p>Looking forward, I&rsquo;m excited to explore building extensions around the flow CLI, from allowing users to BYO-vault to providing a local browser-based UI for executing workflows and discovering what&rsquo;s on your machine.</p>
<p>flow is a reflection of my passion for crafting tools that make developers&rsquo; lives easier. What started as a personal project has grown into something I believe can help other developers take control of their development experience. I invite you to <a href="https://flowexec.io/#/development">contribute</a>, star the <a href="https://github.com/jahvon/flow">repo</a> to show your support, and to open issues to report bugs or suggest features!</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
